/*
* IBM Confidential
* (C) Copyright IBM Corp. 2011 All Rights Reserved.
*/
package com.ibm.crm.tma.adapter.mq.testplan.impl;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.Timer;

import com.ibm.crm.tma.adapter.messagelog.TMAMessageLog;
import com.ibm.crm.tma.adapter.mq.testplan.DefaultTimerTask;
import com.ibm.crm.tma.adapter.mq.testplan.MainTimerCancellerTimerTask;
import com.ibm.crm.tma.adapter.mq.testplan.PlanExecuter;
import com.ibm.crm.tma.adapter.mq.testplan.PlanExecuterException;
import com.ibm.crm.tma.adapter.mq.testplan.TestOperation;
import com.ibm.crm.tma.adapter.mq.testplan.dto.Peak;
import com.ibm.crm.tma.adapter.mq.testplan.dto.Plan;

/**
 * <p>
 * This class is a concrete implementation of the PlanExecuter interface.
 * It provides a concrete implementation of the execute() method defined
 * in the interface by using the java.util.Timer to schedule DefaultTimerTasks
 * with the passed-in operation.
 * </p>
 * <p>
 * Thread Safety:
 * This class is thread safe as it is immutable. its inner state can not be
 * modified after construction, all its attributes are initialized during
 * construction and never changed after that.
 * </p>
 * @author Schpotsky, TCSDEVELOPER
 * @version 1.0
*/
public class PlanExecuterImpl implements PlanExecuter {
    /**
     * <p>
     * The TMAMessageLog instance to be used by this class to log execution
     * information/errors to the common log file. (for more information about
     * logging, please refer to the section 1.3.1 of the CS).
     * Initialized during the construction and never changed
     * after that.
     * Can not be null.
     * It is used in execute() and validate() methods.
     * </p>
    */
    private static final TMAMessageLog  TMA_MESSAGE_LOG =  TMAMessageLog.getMessageLogInstance("TMATestProcessMenu");
    /**
     * <p>
     * Represents a time lag in seconds after which the test plan will be
     * intitiated starting from System.currentTimeMillis(). This used to
     * prevent having undesired behaviour when scheduling a task with first
     * execution time that is prior to the current system time. (To understand
     * why this is used please refer to section 1.3.2 of the CS).
     * It is initialized in the constructor and never changed after that.
     * (The constructor sets it by default to 5 seconds).
     * Can not be non positive.
     * It is used in execute().
     * </p>
    */
    private final int testPlanInitiationTimeLag;

    /**
     * <p>
     * Default 5 seconds for testPlanInitiationTimeLag.
     * It is used in constructor.
     * </p>
     */
    private static final int DEFAULT_INIT_TIME_LAG = 5;
    /**
     * <p>
     * Creates an instance of this class and sets instance fields
     *  to default values.
     * </p>
     * Implementation Notes:
     * 1. testPlanInitiationTimeLag = 5;
    */
    public PlanExecuterImpl() {
        if (TMA_MESSAGE_LOG == null) {
            throw new IllegalArgumentException("TMA_MESSAGE_LOG "
                    + "cannot be null");
        }
        this.testPlanInitiationTimeLag = DEFAULT_INIT_TIME_LAG;
    }
    /**
     * <p>
     * Creates an instance of this class and sets instance fields to values
     *  retrieved from the passed-in properties.
     * (for configuration parameters description, please refer to section
     *  3.2.1 of the CS).
     * </p>
     * @throws IllegalArgumentException
     *             If properties is null.
     * @throws PlanExecuterConfigurationException
     *             If the passed-in properties has invalid values (see
     *              implementation notes).
     * Implementation Notes:
     * 1. initTimeLag = Integer.parseInt(properties
     * .getProperty("TestPlanInitiationTimeLag"));
     * // if it can not be parsed as int then throw
     *  PlanExecuterConfigurationException
     * 2. if (initTimeLag > 0) then
     *    2.1 - this.testPlanInitiationTimeLag = initTimeLag;
     * 3. else
     *    3.1 - throw PlanExecuterConfigurationException with a
     *    meaningful error message;
     * @param properties
     *            The java.util.Properties instance containing
     *             the configuration properties of this class.
    */
    public PlanExecuterImpl(final Properties properties)
            throws PlanExecuterConfigurationException {
        if (TMA_MESSAGE_LOG == null) {
            throw new IllegalArgumentException("TMA_MESSAGE_LOG "
                    + "cannot be null");
        }
        if (properties == null) {
            TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                    PlanExecuterImpl.class.getName(),
                    "PlanExecuterImpl(final Properties properties)",
                    "Properties cannot be null when constructing"
                    + " instance of PlanExecuterImpl");
            throw new IllegalArgumentException("Properties cannot be null "
                    + "when constructing instance of PlanExecuterImpl");
        }

        try {
            int initTimeLag = Integer.parseInt(properties
                    .getProperty("TestPlanInitiationTimeLag"));
            if (initTimeLag > 0) {
                this.testPlanInitiationTimeLag = initTimeLag;
            } else {
                  TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                          PlanExecuterImpl.class.getName(),
                            "PlanExecuterImpl(final Properties properties)",
                            "TestPlanInitiationTimeLag cannot be 0"
                             + " or negative");
                throw new PlanExecuterConfigurationException("Test"
                        + "PlanInitiationTimeLag cannot be 0 or"
                        + " negative");
            }
        } catch (NumberFormatException e) {
            TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                    PlanExecuterImpl.class.getName(),
                    "PlanExecuterImpl(final Properties properties)",
                    "Fail to parse result string from properties"
                    + ".getProperty(\"TestPlanInitiationTimeLag\")");
            throw new PlanExecuterConfigurationException("Fail"
            		+ " to parse result string from properties"
            		+ ".getProperty(\"TestPlanInitiationTimeLag\")", e);
        }
    }
    /**
     * <p>
     * Executes the test plan represented by the passed-in plan
     *  using the passed-in test operation.
     * This method uses java.util.DefaultTimerTask scheduled
     *  using java.util.Timer for executing the test plan operations.
     * It uses the aggregated TMAMessageLog to log execution
     *  debug/errors and information to the common log file
     *  (for more information about logging please refer to
     *  the section 1.3.1 of the CS).
     * </p>
     * Parameters :
     * plan : The Plan instance representing the test plan to be executed.
     * operation : The TestOperation to be executed repetitively
     *  using the configuration specified in the plan instance .
     *
     * Throws:
     * IllegalArgumentException : if plan or operation is null.
     * InvalidTestPlanException : if the passed-in plan is not
     *  valid (see validation rules in validateTestPlanConfiguration() method).
     * PlanExecuterException : if any other error occurred.
     *
     * Implementation Notes :
     * // logging should be performed in this method as described
     * in the section 1.3.1 of the CS.
     *
     * 1. // validate the test plan configuration data.
     *   1.1 - validateTestPlanConfiguration(plan);
     *   // if InvalidTestPlanException is thrown,
     *    let it propagate to the caller.
     * 2. // get the plan configuration data.
     *   2.1 - int numberOfOperations = plan.getTotalAmount();
     *   2.2 - int planDuration = plan.getDuration();
     *   2.3 - List<Peak> peaks = plan.getPeaks();
     * 3. // get the test plan initiation time in milliseconds
     *  (= current time + the configured time lag)
     *   3.1 - long planInitiationTimeMillis = System.currentTimeMillis()
     *    + testPlanInitiationTimeLag * 1000;
     *        // The testPlanInitiationTimeLag is added to the current time
     *         in order to shift the initiation time of the test plan to
     *          avoid problems when scheduling the tasks.
     *       //  if   timer.scheduleAtFixedRate(TimerTask,  Date , period)
     *        is called with a Date (firstTime) which is prior to systemTime
     *         = System.currentTimeMillis()  ( i.e. firstTime < System
     *         .currentTimeMillis() )  then
     *       //  at the first execution of the task  It will additionally
     *        execute (will do a "catch up")  the task n times where n =
     *         1 + (  (systemTime - firstTime) / period )
     *       // for more information about the reason of shifting the test
     *        plan initiation time please refer to the CS section 1.3.2 and
     *         check the provided file TimerTaskTest.java
     * 4. // create the Timer to be used for scheduling the operations.
     *   Timer timer = new Timer();
     * 5. // initialize the counter of number of operations in all peaks.
     *    5.1 - int allPeaksSize = 0;
     * 6. // schedule the operations to be executed during each peak duration.
     *   For each peak in peaks Do
     *     6.1 - offset = peak.getOffset(); // in minutes
     *     6.2 - firstTime = planInitiationTimeMillis + (offset *60*1000);
     *      // in milliseconds
     *     6.3 - peakDuration = peak.getDuration();
     *     6.4 - peakSize = peak.getSize(); // i.e. the number of operations
     *      that will be executed during this specific peak.
     *     6.5 - // add the peakSize to allPeaksSize counter
     *             allPeaksSize += peakSize;
     *     6.6 - // compute the period of operations execution during
     *      this specific peak. (should be in milliseconds, please see
     *       the java.util.Timer.scheduleAtFixedRate() javadoc).
     *              period =  (peakDuration / peakSize) * 60 * 1000;
     *     6.7 - // create an instance of DefaultTimerTask with the
     *      passed-in operation.
     *             defaultTimerTask = new DefaultTimerTask(operation);
     *     6.8 - // schedule the created task for the specific peak.
     *              timer.scheduleAtFixedRate(defaultTimerTask,
     *                new Date(firstTime), period );
     * 7. // compute the number of remaining operations to be executed
     *  during the non-peak duration.
     *    7.1 - int remainingOperationsCount = numberOfOperations
     *     - allPeaksSize;
     * 8. // schedule the remaining operations smoothly around the
     *  whole test plan duration
     *    if( remainingOperationsCount > 0 ) then
     *      8.1 - // compute the period of operations execution
     *            period =  (planDuration / remainingOperationsCount)
     *             *60 * 1000; // in milliseconds
     *      8.2 - // create a new instance of DefaultTimerTask with
     *       the passed-in operation.
     *            nonPeakDurationTimerTask = new DefaultTimerTask(operation);
     *      8.3 // schedule the task
     *           timer.scheduleAtFixedRate(nonPeakDurationTimerTask,
     *            new Date(planInitiationTimeMillis), period );
     * 9. // schedule the timer task to be used for cancelling the
     *  timer after the end of the test plan duration.
     *   9.1 - // Compute the time on which the timer will be
     *    cancelled. (after the end of the test plan duration)
     *           long timerCancellationTime = planInitiationTimeMillis
     *            + (planDuration * 60 * 1000); // in milliseconds
     *   9.2 - // create an Object instance to synchronize on
     *    (this method should not return until the test plan
     *     duration has expired)
     *          Object synchronizationObject = new Object();
     *   9.3 - // create an instance of the MainTimerCancellerTimerTask
     *    to be scheduled for cancelling the main timer created above.
     *         MainTimerCancellerTimerTask mainTimerCancellerTimerTask
     *          = new MainTimerCancellerTimerTask(timer, synchronizationObject);
     *   9.4 - // schedule the canceller timer task.
     *        timer.schedule(mainTimerCancellerTimerTask, new Date
     *        (timerCancellationTime));
     * 10. // wait until the duration of the test plan is expired
     *  (i.e until the canceller timer task is executed).
     *      10.1 - synchronized(synchronizationObject ){
     *           10.1.1 - synchronizationObject.wait();
     *             }
     * @param operation
     * @param plan
     * @throws InvalidTestPlanException
     * @throws PlanExecuterException
    */
    public final void execute(final Plan plan,
            final TestOperation operation)
            throws InvalidTestPlanException, PlanExecuterException {
        if (plan == null) {
            TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                    PlanExecuterImpl.class.getName(),
                    "execute(final Plan plan, final TestOperation operation)",
                    "Parameter plan cannot be null");
            throw new IllegalArgumentException("Parameter plan cannot be null");
        }

        if (operation == null) {
            TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                    PlanExecuterImpl.class.getName(),
                    "execute(final Plan plan, final TestOperation operation)",
                    "Parameter operation cannot be null");
            throw new IllegalArgumentException("Parameter"
            		+ " operation cannot be null");
        }

        validateTestPlanConfiguration(plan);
        int numberOfOperations = plan.getTotalAmount();
        int planDuration = plan.getDuration();
        List<Peak> peaks = plan.getPeaks();
        long planInitiationTimeMillis = System.currentTimeMillis()
                                        + testPlanInitiationTimeLag * 1000;
        Timer timer = new Timer();
        int allPeaksSize = 0;
        if (peaks != null) {
            for (Peak peak : peaks) {
                int offset = peak.getOffset();
                long firstTime = planInitiationTimeMillis
                                + (offset * 60 * 1000);
                int peakDuration = peak.getDuration();
                int peakSize = peak.getSize();
                allPeaksSize += peakSize;

                long period = (long) ((peakDuration / (double) peakSize)
                               * 60 * 1000);

                DefaultTimerTask defaultTimerTask =
                        new DefaultTimerTask(operation);

                timer.schedule(defaultTimerTask, new Date(firstTime), period);
            }
        } else {
                TMA_MESSAGE_LOG.logWarning("TMA_PLAN_MANAGER_W",
                        PlanExecuterImpl.class.getName(),
                        "validateTestPlanConfiguration(final Plan plan)",
                        "Test Operation contains does not contain any peak");
        }
        int remainingOperationsCount =
                numberOfOperations - allPeaksSize;

        if (remainingOperationsCount > 0) {
            long period = (long) ((planDuration
                    / (double) remainingOperationsCount) * 60 *1000);

            DefaultTimerTask nonPeakDurationTimerTask =
                    new DefaultTimerTask(operation);

            timer.scheduleAtFixedRate(nonPeakDurationTimerTask,
                    new Date(planInitiationTimeMillis), period);
        }
        long timerCancellationTime = planInitiationTimeMillis
                + (planDuration * 60 * 1000);
        Object synchronizationObject = new Object();
        MainTimerCancellerTimerTask mainTimerCancellerTimerTask =
                new MainTimerCancellerTimerTask(timer,
                        synchronizationObject);

        timer.schedule(mainTimerCancellerTimerTask,
                new Date(timerCancellationTime));

        synchronized (synchronizationObject) {
            try {
                synchronizationObject.wait();
            } catch (InterruptedException e) {

                TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                        PlanExecuterImpl.class.getName(),
                        "execute(final Plan plan, final"
                        + " TestOperation operation)",
                        e.getLocalizedMessage());

                throw new PlanExecuterException(
                        e.getLocalizedMessage(), e);
            }
        }

    }
    /**
     * Purpose:
     * This method checks the validity of the provided test plan data :
     * 1) The sum of one peak's offset and duration should not be
     *  larger than the duration of the plan.
     * 2) The sum of the sizes of all peaks should not be larger
     *  than the total number defined in plan.
     * 3) Each peak in the test plan should have a strictly positive
     *  duration and size.
     * 4) Each peak in the test plan should have an offset >= 0.
     *
     * Parameters:
     * plan : The Plan instance to be validated.
     *
     * Throws :
     * IllegalArgumentException : if the input parameter is null.
     * InvalidTestPlanException : if the passed-in plan instance
     *  is invalid (i.e. does not conform to the validation rules
     *   mentioned above).
     *
     * Implementation Notes:
     * // logging should be performed in this method as described
     *  in the section 1.3.1 of the CS.
     *
     * 1. // get the plan duration
     *    planDuration = plan.getDuration();
     * 2. // get the number of operations in the plan
     *    numberOfOperations = plan.getTotalAmount();
     * 3. // intialize the counter of the number of operations
     *  in all peaks of the plan.
     *    int allPeaksSize = 0;
     * 4. // get the peaks List from the plan
     *    List<Peaks> peaks = plan.getPeaks();
     * 5. // validate each peak and add its size to the allPeaksSize
     *   foreach ( peak in peaks ) Do
     *     5.1 - offset = peak.getOffset();
     *     5.2 - size   = peak.getSize();
     *     5.3 - duration = peak.getDuration();
     *     5.4 - if (offset + duration > planDuration ) then
     *        5.4.1 - throw InvalidTestPlanException with a
     *         meaningful error message.
     *     5.5 - if ( offset < 0 ) then
     *        5.5.1 - throw InvalidTestPlanException with a
     *         meaningful error message.
     *     5.6 - if (one of duration or size is not strictly positive) then
     *        5.6.1 - throw InvalidTestPlanException with a
     *         meaningful error message.
     *     5.7. - allPeaksSize += size;
     * 6. // check if the sum of the sizes of all peaks
     *  is not larger than the number of operations in the plan.
     *    if (allPeaksSize > numberOfOperations ) then
     *     6.1 - throw InvalidTestPlanException with a meaningful error message.
     * @param plan
     * @throws InvalidTestPlanException
    */
    private void validateTestPlanConfiguration(final Plan plan)
            throws InvalidTestPlanException{
        if (plan == null) {
            TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                    PlanExecuterImpl.class.getName(),
                    "validateTestPlanConfiguration(final Plan plan)",
                    "Parameter plan cannot be null");
            throw new IllegalArgumentException("Parameter plan cannot be null");
        }

        int planDuration = plan.getDuration();
        int numberOfOperations = plan.getTotalAmount();
        int allPeaksSize = 0;
        List<Peak> peaks = plan.getPeaks();
        if (peaks != null) {
            for (Peak peak : peaks) {
                int offset = peak.getOffset();
                int size = peak.getSize();
                int duration = peak.getDuration();
                if (offset + duration > planDuration) {
                    TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                            PlanExecuterImpl.class.getName(),
                            "validateTestPlanConfiguration(final Plan plan)",
                            "Peak offset duration cannot exceed "
                            + "plan duration");
                    throw new InvalidTestPlanException("Peak offset"
                            + " duration cannot exceed"
                            + " plan duration");
                }
                if (offset < 0) {
                    TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                            PlanExecuterImpl.class.getName(),
                            "validateTestPlanConfiguration(final Plan plan)",
                            "Peak offset cannot be a negative number");
                    throw new InvalidTestPlanException("Peak offset "
                    		+ "cannot be a negative number");
                }
                if (duration <= 0 || size <= 0) {
                    TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                            PlanExecuterImpl.class.getName(),
                            "validateTestPlanConfiguration(final Plan plan)",
                            "Peak duration and size have to be"
                            + " strictly positive");
                    throw new InvalidTestPlanException("Peak duration and size "
                    		 + "have to be strictly positive");
                }

                allPeaksSize += size;
            }
        }

        if (allPeaksSize > numberOfOperations) {
            TMA_MESSAGE_LOG.logError("TMA_PLAN_MANAGER_E",
                     PlanExecuterImpl.class.getName(),
                    "validateTestPlanConfiguration(final Plan plan)",
                    "Peak size cannot be greater than number of operations");
            throw new InvalidTestPlanException("Peak size cannot be"
            		+ " greater than number of operations");
        }
    }
}

